import logging
import os
import re
import urllib
import urlparse
import datetime
import traceback


from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.api import taskqueue
from google.appengine.api import urlfetch

from django.conf import settings as django_settings
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.core.urlresolvers import reverse

from libs.BeautifulSoup import BeautifulSoup

from app import models
from app import forms

IS_DEV = os.environ['SERVER_SOFTWARE'].startswith('Dev')  # Development server


### Helper functions ###

def respond(request, template, params=None):
	"""Helper to render a response, passing standard stuff to the response.
	
	Args:
	  request: The request object.
	  template: The template name; '.html' is appended automatically.
	  params: A dict giving the template parameters; modified in-place.
	
	Returns:
	  Whatever render_to_response(template, params) returns.
	
	Raises:
	  Whatever render_to_response(template, params) raises.
	"""

	if params is None:
		params = {}
	params['user'] = request.user
	params['is_admin'] = request.user_is_admin
	params['is_dev'] = IS_DEV
	params['media_url'] = django_settings.MEDIA_URL
	full_path = request.get_full_path().encode('utf-8')
	if request.user is None:
		params['sign_in'] = users.create_login_url(full_path)
	else:
		params['sign_out'] = users.create_logout_url(full_path)

	return render_to_response(template, params,
						context_instance=RequestContext(request))

def _clean_int(value, default, min_value=None, max_value=None):
	"""Helper to cast value to int and to clip it to min or max_value.

	Args:
		value: Any value (preferably something that can be casted to int).
		default: Default value to be used when type casting fails.
		min_value: Minimum allowed value (default: None).
		max_value: Maximum allowed value (default: None).
	
	Returns:
		An integer between min_value and max_value.
	"""
	if not isinstance(value, (int, long)):
		try:
			value = int(value)
		except (TypeError, ValueError):
			value = default
	if min_value is not None:
		value = max(min_value, value)
	if max_value is not None:
		value = min(value, max_value)
	return value


def _can_view_link(user, link):
	if user is None:
		return not link.private
	return (not link.private
			or link.owner == user
			or user.email() in link.collaborator_emails())


class HttpTextResponse(HttpResponse):
	def __init__(self, *args, **kwargs):
		kwargs['content_type'] = 'text/plain; charset=utf-8'
		super(HttpTextResponse, self).__init__(*args, **kwargs)
	
	
class HttpHtmlResponse(HttpResponse):
	def __init__(self, *args, **kwargs):
		kwargs['content_type'] = 'text/html; charset=utf-8'
		super(HttpHtmlResponse, self).__init__(*args, **kwargs)


### Decorators for request handlers ###

def post_required(func):
	"""Decorator that returns an error unless request.method == 'POST'."""

	def post_wrapper(request, *args, **kwds):
		if request.method != 'POST':
			return HttpTextResponse('This requires a POST request.', status=405)
		return func(request, *args, **kwds)

	return post_wrapper


def login_required(func):
	"""Decorator that redirects to the login page if you're not logged in."""

	def login_wrapper(request, *args, **kwds):
		if request.user is None:
			return HttpResponseRedirect(
				users.create_login_url(request.get_full_path().encode('utf-8')))
		return func(request, *args, **kwds)

	return login_wrapper


def link_required(func):
	"""Decorator that processes the link_id handler argument."""

	def link_wrapper(request, link_id, *args, **kwds):
		link = models.Link.get_by_id(int(link_id))
		if link is None:
			return HttpTextResponse(
				'No link exists with that id (%s)' % link_id, status=404)
		if link.private:
			if request.user is None:
				return HttpResponseRedirect(
						users.create_login_url(request.get_full_path().encode('utf-8')))
			if not _can_view_link(request.user, link):
				return HttpTextResponse(
						'You do not have permission to view this link', status=403)
		request.link = link
		return func(request, *args, **kwds)

	return link_wrapper


def user_key_required(func):
	"""Decorator that processes the user handler argument."""

	def user_key_wrapper(request, user_key, *args, **kwds):
		user_key = urllib.unquote(user_key)
		if '@' in user_key:
			request.user_to_show = users.User(user_key)
		else:
			account = models.Account.get_account_for_nickname(user_key)
			if not account:
				logging.info("account not found for nickname %s" % user_key)
				return HttpTextResponse(
						'No user found with that key (%s)' % urllib.quote(user_key),
						status=404)
			request.user_to_show = account.user
		return func(request, *args, **kwds)

	return user_key_wrapper


def owner_required(func):
	"""Decorator that insists you own the link.

	It must appear after link_required.
	"""

	@login_required
	def owner_wrapper(request, *args, **kwds):
		if not (request.link.owner == request.user
				or request.link.is_collaborator(request.user)):
			return HttpTextResponse('You do not own this link', status=403)
		return func(request, *args, **kwds)

	return owner_wrapper


def link_owner_required(func):
	"""Decorator that processes the link_id argument and insists you own it."""

	@link_required
	@owner_required
	def link_owner_wrapper(request, *args, **kwds):
		return func(request, *args, **kwds)

	return link_owner_wrapper


def link_editor_required(func):
	"""Decorator that processes the link_id argument and insists the user has
	permission to edit it."""

	@login_required
	@link_required
	def link_editor_wrapper(request, *args, **kwds):
		if not request.link.user_can_edit(request.user):
			return HttpTextResponse(
					'You do not have permission to edit this link', status=403)
		return func(request, *args, **kwds)

	return link_editor_wrapper


def index(request):
	"""/ - Show a list of links"""
	if request.user is None:
		return all(request, index_call=True)
	else:
		return mine(request)


DEFAULT_LIMIT = 20

def _url(path, **kwargs):
	"""Format parameters for query string.

	Args:
		path: Path of URL.
		kwargs: Keyword parameters are treated as values to add to the query
			parameter of the URL.	If empty no query parameters will be added to
			path and '?' omitted from the URL.
	"""
	if kwargs:
		encoded_parameters = urllib.urlencode(kwargs)
		if path.endswith('?'):
			# Trailing ? on path.	Append parameters to end.
			return '%s%s' % (path, encoded_parameters)
		elif '?' in path:
			# Append additional parameters to existing query parameters.
			return '%s&%s' % (path, encoded_parameters)
		else:
			# Add query parameters to path with no query parameters.
			return '%s?%s' % (path, encoded_parameters)
	else:
		return path


def _inner_paginate(request, links, template, extra_template_params):
	"""Display paginated list of links.

	Takes care of the private bit.

	Args:
		request: Request containing offset and limit parameters.
		links: Links to be displayed.
		template: Name of template that renders link page.
		extra_template_params: Dictionary of extra parameters to pass to page
			rendering.

	Returns:
		Response for sending back to browser.
	"""
	visible_links = [link for link in links if _can_view_link(request.user, link)]
#	_load_users_for_links(visible_links)
	params = {
		'links': visible_links,
		'limit': None,
		'newest': None,
		'prev': None,
		'next': None,
		'nexttext': '',
		'first': '',
		'last': '',
	}
	if extra_template_params:
		params.update(extra_template_params)
	return respond(request, template, params)


def _paginate_links(page_url,
					request,
					query,
					template,
					extra_nav_parameters=None,
					extra_template_params=None):
	"""Display paginated list of links.

	Args:
		page_url: Base URL of link page that is being paginated.	Typically
			generated by calling 'reverse' with a name and arguments of a view
			function.
		request: Request containing offset and limit parameters.
		query: Query over links.
		template: Name of template that renders link page.
		extra_nav_parameters: Dictionary of extra parameters to append to the
			navigation links.
		extra_template_params: Dictionary of extra parameters to pass to page
			rendering.

	Returns:
		Response for sending back to browser.
	"""
	offset = _clean_int(request.GET.get('offset'), 0, 0)
	limit = _clean_int(request.GET.get('limit'), DEFAULT_LIMIT, 1, 100)

	nav_parameters = {'limit': str(limit)}
	if extra_nav_parameters is not None:
		nav_parameters.update(extra_nav_parameters)

	params = {
		'limit': limit,
		'first': offset + 1,
		'nexttext': 'Older',
	}
	# Fetch one more to see if there should be a 'next' link
	links = query.fetch(limit+1, offset)
	if len(links) > limit:
		del links[limit:]
		params['next'] = _url(page_url, offset=offset + limit, **nav_parameters)
	params['last'] = len(links) > 1 and offset+len(links) or None
	if offset > 0:
		params['prev'] = _url(page_url, offset=max(0, offset - limit),
				**nav_parameters)
	if offset > limit:
		params['newest'] = _url(page_url, **nav_parameters)
	if extra_template_params:
		params.update(extra_template_params)
	return _inner_paginate(request, links, template, params)


def _paginate_links_with_cursor(page_url,
								request,
								query,
								limit,
								template,
								extra_nav_parameters=None,
								extra_template_params=None):
	"""Display paginated list of links using a cursor instead of offset.

	Args:
		page_url: Base URL of link page that is being paginated.	Typically
			generated by calling 'reverse' with a name and arguments of a view
			function.
		request: Request containing offset and limit parameters.
		query: Query over links.
		limit: Maximum number of links to return.
		template: Name of template that renders link page.
		extra_nav_parameters: Dictionary of extra parameters to append to the
			navigation links.
		extra_template_params: Dictionary of extra parameters to pass to page
			rendering.

	Returns:
		Response for sending back to browser.
	"""
	links = query.fetch(limit)
	nav_parameters = {}
	if extra_nav_parameters:
		nav_parameters.update(extra_nav_parameters)
	nav_parameters['cursor'] = query.cursor()

	params = {
		'limit': limit,
		'cursor': nav_parameters['cursor'],
		'nexttext': 'Next',
	}
	# Fetch one more to see if there should be a 'next' link. Do it in a separate
	# request so we have a valid cursor.
# TODO: this doesn't work somehow
	if query.fetch(1):
		params['next'] = _url(page_url, **nav_parameters)
	if extra_template_params:
		params.update(extra_template_params)
	return _inner_paginate(request, links, template, params)


def all(request, index_call=False):  # @ReservedAssignment
	"""/all - Show a list of up to DEFAULT_LIMIT recent links."""
	nav_parameters = {}

	query = models.Link.all().filter('private =', False)
	query.order('-modified')

	return _paginate_links(
			reverse(all),
			request,
			query,
			'all.html',
			extra_nav_parameters=nav_parameters,
			extra_template_params=dict())


@login_required
def mine(request):
	"""/mine - Show a list of links created by the current user."""
	request.user_to_show = request.user
	return _show_user(request)


@user_key_required
def show_user(request):
	"""/user - Show the user's links"""
	return _show_user(request)


def _show_user(request):
	user = request.user_to_show

	links = [link for link in db.GqlQuery(
					'SELECT * FROM Link '
					'WHERE owner = :1 '
					'ORDER BY modified DESC '
					'LIMIT 100',
					user)
			if _can_view_link(request.user, link)]

	account = models.Account.get_account_for_user(request.user_to_show)

	return respond(request, 'user.html', {
		'account': account,
		'links': links,
		})


@login_required
def new(request):
	"""/new - Create a new link.

	GET shows a blank form, POST processes it.
	"""
	if request.method != 'POST':
		form = forms.NewForm()
		return respond(request, 'new.html', {'form': form})

	form = forms.NewForm(request.POST, request.FILES)
	link = _make_new(request, form)
	if link is None:
		return respond(request, 'new.html', {'form': form})
	else:
		return HttpResponseRedirect(reverse(show, args=[link.key().id()]))


def _make_new(request, form):
	"""Create new link and fill relevant fields from given form data."""
	if not form.is_valid():
		return None

	link = models.Link(
			url=form.cleaned_data['url'],
			owner=request.user,
			description=form.cleaned_data['description'],
			tags=form.cleaned_data['tags'],
			attr_data=form.cleaned_data['attributes'],
			private=form.cleaned_data.get('private', False),
	)
	link.put()
	add_task_for_metainfo(link)

	return link


@link_required
def show(request):
	"""/<link> - Show a link."""
	link = request.link
	attributes = link.attributes_as_dict()
	favicon_base64 = link.favicon.encode("base64") if link.favicon else ''
	params = {
		'link': link,
		'attributes': attributes,
		'favicon_base64': favicon_base64,
	}
	return respond(request, 'link.html', params)

@link_editor_required
def edit(request):
	"""/<link>/edit - Edit a link."""
	link = request.link
	if request.method != 'POST':
		initial={
				'url': link.url,
				'description': link.description,
				'tags': ','.join(link.tags),
				'attributes': link.attr_data,
				'private': link.private,
		}
		form = forms.EditForm(initial=initial)
		return respond(request, 'edit.html', {'link': link, 'form': form})

	form = forms.EditForm(request.POST)
	if not form.is_valid():
		return respond(request, 'edit.html', {'link': link, 'form': form})

	link.url = form.cleaned_data['url']
	link.description = form.cleaned_data['description']
	link.tags = form.cleaned_data['tags']
	link.attr_data = form.cleaned_data['attributes']
	link.private = form.cleaned_data.get('private', False)
	link.put()

	add_task_for_metainfo(link)

	# wait for the task to complete? (use a channel?)
	# https://developers.google.com/appengine/docs/python/channel/

	return HttpResponseRedirect(reverse(show, args=[link.key().id()]))


def add_task_for_metainfo(link):
	# create a task for retrieving metainfo
	taskqueue.add(
			url='/retrieve_metainfo', 
			method='GET', 
			params={'key': link.key().id()},
			retry_options=taskqueue.TaskRetryOptions(task_age_limit=300),
			)


@post_required
@link_owner_required
def delete(request):
	"""/<link>/delete - Delete a link. There is no way back."""
	link = request.link
	tbd = [link]
	db.delete(tbd)
	return HttpResponseRedirect(reverse(mine))


def browse(request):
	return respond(request, 'browse.html')


def search(request):
	"""/search - Search for links.

	Returns HTTP 500 if the corresponding index is missing.
	"""
	if request.method == 'GET':
		form = forms.SearchForm(request.GET)
		if not form.is_valid() or not request.GET:
			return respond(request, 'search.html', {'form': form})
	else:
		form = forms.SearchForm(request.POST)
		if not form.is_valid():
			return HttpTextResponse('Invalid arguments', status=400)

	limit = 30
	q = models.Link.all()
	cleaned_data = form.cleaned_data
	if cleaned_data['cursor']:
		q.with_cursor(cleaned_data['cursor'])

	if cleaned_data['tags']:
		for tag in cleaned_data['tags']:
			q.filter('tags = ', tag)

	if cleaned_data['owner']:
		if '@' in cleaned_data['owner']:
			owner = users.User(cleaned_data['owner'])
		else:
			account = models.Account.get_account_for_nickname(cleaned_data['owner'])
			if account:
				owner = account.user
			else:
				logging.info("account not found for nickname %s" % cleaned_data['owner'])
				return HttpTextResponse('No user found with that name (%s)'
						% urllib.quote(cleaned_data['owner']), status=404)
		if owner:
			q.filter('owner = ', owner)

	if cleaned_data['domain']:
		q.filter('superdomains = ', cleaned_data['domain'])
	if cleaned_data['private'] is not None:
		q.filter('private = ', cleaned_data['private'])

	default_sort = '-modified'
	sorted_by = cleaned_data['order'] or default_sort
	q.order(sorted_by)

	# Update the cursor value in the result.
	nav_params = dict((k, v) for k, v in cleaned_data.iteritems() if v is not None)
	return _paginate_links_with_cursor(
			reverse(search),
			request,
			q,
			limit,
			'search_results.html',
			extra_nav_parameters=nav_params)

def retrieve_metainfo(request):
	"""	Worker URL for a task.
	/retrieve_metainfo?key=<key>

	Query string parameters:
		key: The numeric key of the 'link' entity in the datastore.

	Fetches the specified url, parses the html response 
	and updates the corresponding entity in the datastore.

	Returns HTTP 500 if unable to complete the task.
	"""
	# http://g.etfv.co - retrieving favicon.ico example
	key = int(request.GET["key"])
	link = models.Link.get_by_id(key)
	
	if not link:
		return HttpTextResponse('Error: Link entity with the specified key (%d) does not exist' % key, status=500)

	url = link.url
	try:
		response = urlfetch.fetch(url)
	except:
		logging.error('Exception. ' + traceback.format_exc())
		return HttpTextResponse('Exception. ' + traceback.format_exc(), status=500)

	if not response.status_code == 200:
		return HttpTextResponse('Error: Fetching URL %s resulted in HTTP status code %d.' % (url, response.status_code), status=500)

	html = response.content


	try:
		soup = BeautifulSoup(html)
	except:
		logging.error('Exception. ' + traceback.format_exc())
		return HttpTextResponse('Exception. ' + traceback.format_exc(), status=500)

	# retrieved
	retrieved = datetime.datetime.now()
	link.retrieved = retrieved

	# final_url
	final_url = response.final_url if response.final_url else url
	link.final_url = final_url

	# favicon
	favicon = _retrieve_linked_favicon(soup, final_url)
	if not favicon:
		favicon = _retrieve_favicon_at_root(final_url)
		if not favicon:
			favicon = None
	link.favicon = favicon

	# title
	if soup.title:
		title = soup.title.string
		# replace any whitespace sequence with a single space character
		title = unicode(' '.join(title.split()))
	else:
		title = None
	link.title = title

	# meta_description
	meta_description_tag = soup.find('meta', attrs={'name': re.compile('^description$', flags=re.IGNORECASE)})
	meta_description = meta_description_tag['content'] if meta_description_tag else None
	link.meta_description = meta_description
	logging.debug('meta_description: %s' % meta_description)

	# meta_keywords
	meta_keywords_tag = soup.find('meta', attrs={'name': re.compile('^keywords$', flags=re.IGNORECASE)})
	if meta_keywords_tag:
		meta_keywords = meta_keywords_tag['content'].split(',')
		meta_keywords = map(unicode.strip, meta_keywords)
	else:
		meta_keywords = []
	link.meta_keywords = meta_keywords
	logging.debug('meta_keywords: %s' % meta_keywords)

	# linked_pages
	# preview

	link.put()

	favicon_base64 = favicon.encode("base64") if favicon else ''
	return HttpHtmlResponse('URL: %s; response code: %s; content-length: %s bytes; title: %s; favicon: <img alt="favicon" src="data:image/x-icon;base64,%s" />' 
					% (url, response.status_code, len(response.content), link.title, favicon_base64))


def _retrieve_linked_favicon(soup, final_url):
	# Return favicon as a sequence of bytes
	favicon = None
	faviconHTML = soup.find("link", rel=re.compile("^(shortcut|icon|shortcut icon)$", re.IGNORECASE))

	if faviconHTML:
		faviconHref = faviconHTML.get("href")

		if faviconHref:
			faviconPath = urlparse.urljoin(final_url, faviconHref)

			try:
				faviconResponse = urlfetch.fetch(faviconPath)
				if _is_valid_icon_response(faviconResponse):
					favicon = faviconResponse.content
			except urlfetch.Error:
				pass
	return favicon

def _retrieve_favicon_at_root(final_url):
	domain = 'http://' + urlparse.urlparse(final_url).netloc
	try:
		response = urlfetch.fetch(domain + '/favicon.ico')
	except:
		return None

	if _is_valid_icon_response(response):
		return response.content
	else:
		return None


def _is_valid_icon_response(iconResponse):
	ICON_MIMETYPE_BLACKLIST = ["application/xml", "text/html"]
	MIN_ICON_LENGTH = 100
	MAX_ICON_LENGTH = 20000

	iconLength = len(iconResponse.content)

	iconContentType = iconResponse.headers.get("Content-Type")
	if iconContentType:
		iconContentType = iconContentType.split(";")[0]

	invalidIconReason = []

	if not iconResponse.status_code == 200:
		invalidIconReason.append("Status code isn't 200")

	if iconContentType in ICON_MIMETYPE_BLACKLIST:
		invalidIconReason.append("Content-Type in ICON_MIMETYPE_BLACKLIST")

	if iconLength < MIN_ICON_LENGTH:
		invalidIconReason.append("Length below MIN_ICON_LENGTH")

	if iconLength > MAX_ICON_LENGTH:
		invalidIconReason.append("Length greater than MAX_ICON_LENGTH")

	if len(invalidIconReason) > 0:
		logging.info("Invalid icon because: %s" % invalidIconReason)
		return False
	else:
		return True

def test(request):
	return HttpTextResponse("this is a test page")

